/*
 * Created on Dec 4, 2009
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package org.gudy.azureus2.core3.tracker.client.impl;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.PEPeerSource;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerDataProvider;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerFactory;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponse;
import org.gudy.azureus2.core3.tracker.client.impl.bt.TRTrackerBTAnnouncerImpl;
import org.gudy.azureus2.core3.tracker.client.impl.dht.TRTrackerDHTAnnouncerImpl;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResult;

import com.aelitis.azureus.core.tracker.TrackerPeerSource;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class TRTrackerAnnouncerMuxer extends TRTrackerAnnouncerImpl {
    private static final int ACT_CHECK_INIT_DELAY = 2500;
    private static final int ACT_CHECK_INTERIM_DELAY = 10 * 1000;
    private static final int ACT_CHECK_IDLE_DELAY = 30 * 1000;
    private static final int ACT_CHECK_SEEDING_SHORT_DELAY = 60 * 1000;
    private static final int ACT_CHECK_SEEDING_LONG_DELAY = 3 * 60 * 1000;

    private TRTrackerAnnouncerFactory.DataProvider f_provider;
    private boolean is_manual;

    private long create_time = SystemTime.getMonotonousTime();

    private CopyOnWriteList<TRTrackerAnnouncerHelper> announcers = new CopyOnWriteList<TRTrackerAnnouncerHelper>();
    private Set<TRTrackerAnnouncerHelper> activated = new HashSet<TRTrackerAnnouncerHelper>();
    private long last_activation_time;
    private Set<String> failed_urls = new HashSet<String>();

    private volatile TimerEvent event;

    private TRTrackerAnnouncerDataProvider provider;
    private String ip_override;
    private boolean complete;
    private boolean stopped;
    private boolean destroyed;

    private TRTrackerAnnouncerHelper last_best_active;
    private long last_best_active_set_time;

    private Map<String, StatusSummary> recent_responses = new HashMap<String, StatusSummary>();

    private TRTrackerAnnouncerResponse last_response_informed;

    protected TRTrackerAnnouncerMuxer(TOTorrent _torrent, TRTrackerAnnouncerFactory.DataProvider _f_provider, boolean _manual)

    throws TRTrackerAnnouncerException {
        super(_torrent);

        try {
            last_response_informed =
                    new TRTrackerAnnouncerResponseImpl(null, _torrent.getHashWrapper(), TRTrackerAnnouncerResponse.ST_OFFLINE,
                            TRTrackerAnnouncer.REFRESH_MINIMUM_SECS, "Initialising");

        } catch (TOTorrentException e) {

            Logger.log(new LogEvent(_torrent, LOGID, "Torrent hash retrieval fails", e));

            throw (new TRTrackerAnnouncerException("TRTrackerAnnouncer: URL encode fails"));
        }

        is_manual = _manual;
        f_provider = _f_provider;

        split();
    }

    protected void split()

    throws TRTrackerAnnouncerException {
        String[] networks = f_provider == null ? null : f_provider.getNetworks();

        TRTrackerAnnouncerHelper to_activate = null;

        synchronized (this) {

            if (stopped || destroyed) {

                return;
            }

            TOTorrent torrent = getTorrent();

            TOTorrentAnnounceURLSet[] sets = torrent.getAnnounceURLGroup().getAnnounceURLSets();

            // sanitise dht entries

            if (sets.length == 0) {

                sets = new TOTorrentAnnounceURLSet[] { torrent.getAnnounceURLGroup().createAnnounceURLSet(new URL[] { torrent.getAnnounceURL() }) };

            } else {

                boolean found_decentralised = false;
                boolean modified = false;

                for (int i = 0; i < sets.length; i++) {

                    TOTorrentAnnounceURLSet set = sets[i];

                    URL[] urls = set.getAnnounceURLs().clone();

                    for (int j = 0; j < urls.length; j++) {

                        URL u = urls[j];

                        if (u != null && TorrentUtils.isDecentralised(u)) {

                            if (found_decentralised) {

                                modified = true;

                                urls[j] = null;

                            } else {

                                found_decentralised = true;
                            }
                        }
                    }
                }

                if (modified) {

                    List<TOTorrentAnnounceURLSet> s_list = new ArrayList<TOTorrentAnnounceURLSet>();

                    for (TOTorrentAnnounceURLSet set : sets) {

                        URL[] urls = set.getAnnounceURLs();

                        List<URL> u_list = new ArrayList<URL>(urls.length);

                        for (URL u : urls) {

                            if (u != null) {

                                u_list.add(u);
                            }
                        }

                        if (u_list.size() > 0) {

                            s_list.add(torrent.getAnnounceURLGroup().createAnnounceURLSet(u_list.toArray(new URL[u_list.size()])));
                        }
                    }

                    sets = s_list.toArray(new TOTorrentAnnounceURLSet[s_list.size()]);
                }
            }

            List<TOTorrentAnnounceURLSet[]> new_sets = new ArrayList<TOTorrentAnnounceURLSet[]>();

            if (is_manual || sets.length < 2) {

                new_sets.add(sets);

            } else {

                List<TOTorrentAnnounceURLSet> list = new ArrayList<TOTorrentAnnounceURLSet>(Arrays.asList(sets));

                // often we have http:/xxxx/ and udp:/xxxx/ as separate groups - keep these together

                while (list.size() > 0) {

                    TOTorrentAnnounceURLSet set1 = list.remove(0);

                    boolean done = false;

                    URL[] urls1 = set1.getAnnounceURLs();

                    if (urls1.length == 1) {

                        URL url1 = urls1[0];

                        String prot1 = url1.getProtocol().toLowerCase();
                        String host1 = url1.getHost();

                        for (int i = 0; i < list.size(); i++) {

                            TOTorrentAnnounceURLSet set2 = list.get(i);

                            URL[] urls2 = set2.getAnnounceURLs();

                            if (urls2.length == 1) {

                                URL url2 = urls2[0];

                                String prot2 = url2.getProtocol().toLowerCase();
                                String host2 = url2.getHost();

                                if (host1.equals(host2)) {

                                    if ((prot1.equals("udp") && prot2.startsWith("http")) || (prot2.equals("udp") && prot1.startsWith("http"))) {

                                        list.remove(i);

                                        new_sets.add(new TOTorrentAnnounceURLSet[] { set1, set2 });

                                        done = true;
                                    }
                                }
                            }
                        }
                    }

                    if (!done) {

                        new_sets.add(new TOTorrentAnnounceURLSet[] { set1 });
                    }
                }
            }

            // work out the difference

            Iterator<TOTorrentAnnounceURLSet[]> ns_it = new_sets.iterator();

            // need to copy list as we modify it and returned list ain't thread safe

            List<TRTrackerAnnouncerHelper> existing_announcers = new ArrayList<TRTrackerAnnouncerHelper>(announcers.getList());

            List<TRTrackerAnnouncerHelper> new_announcers = new ArrayList<TRTrackerAnnouncerHelper>();

            // first look for unchanged sets

            while (ns_it.hasNext()) {

                TOTorrentAnnounceURLSet[] ns = ns_it.next();

                Iterator<TRTrackerAnnouncerHelper> a_it = existing_announcers.iterator();

                while (a_it.hasNext()) {

                    TRTrackerAnnouncerHelper a = a_it.next();

                    TOTorrentAnnounceURLSet[] os = a.getAnnounceSets();

                    if (same(ns, os)) {

                        ns_it.remove();
                        a_it.remove();

                        new_announcers.add(a);

                        break;
                    }
                }
            }

            // first remove dht ones from the equation

            TRTrackerAnnouncerHelper existing_dht_announcer = null;
            TOTorrentAnnounceURLSet[] new_dht_set = null;

            ns_it = new_sets.iterator();

            while (ns_it.hasNext()) {

                TOTorrentAnnounceURLSet[] x = ns_it.next();

                if (TorrentUtils.isDecentralised(x[0].getAnnounceURLs()[0])) {

                    new_dht_set = x;

                    ns_it.remove();

                    break;
                }
            }

            Iterator<TRTrackerAnnouncerHelper> an_it = existing_announcers.iterator();

            while (an_it.hasNext()) {

                TRTrackerAnnouncerHelper a = an_it.next();

                TOTorrentAnnounceURLSet[] x = a.getAnnounceSets();

                if (TorrentUtils.isDecentralised(x[0].getAnnounceURLs()[0])) {

                    existing_dht_announcer = a;

                    an_it.remove();

                    break;
                }
            }

            if (existing_dht_announcer != null && new_dht_set != null) {

                new_announcers.add(existing_dht_announcer);

            } else if (existing_dht_announcer != null) {

                activated.remove(existing_dht_announcer);

                existing_dht_announcer.destroy();

            } else if (new_dht_set != null) {

                TRTrackerAnnouncerHelper a = create(torrent, networks, new_dht_set);

                new_announcers.add(a);
            }

            // create any new ones required

            ns_it = new_sets.iterator();

            while (ns_it.hasNext()) {

                TOTorrentAnnounceURLSet[] s = ns_it.next();

                TRTrackerAnnouncerHelper a = create(torrent, networks, s);

                new_announcers.add(a);
            }

            // finally fix up the announcer list to represent the new state

            Iterator<TRTrackerAnnouncerHelper> a_it = announcers.iterator();

            while (a_it.hasNext()) {

                TRTrackerAnnouncerHelper a = a_it.next();

                if (!new_announcers.contains(a)) {

                    a_it.remove();

                    try {
                        if (activated.contains(a) && torrent.getPrivate() && a instanceof TRTrackerBTAnnouncerImpl) {

                            URL url = a.getTrackerURL();

                            if (url != null) {

                                forceStop((TRTrackerBTAnnouncerImpl) a, networks, url);
                            }
                        }
                    } finally {

                        if (Logger.isEnabled()) {
                            Logger.log(new LogEvent(getTorrent(), LOGID, "Deactivating " + getString(a.getAnnounceSets())));
                        }

                        activated.remove(a);

                        a.destroy();
                    }
                }
            }

            a_it = new_announcers.iterator();

            while (a_it.hasNext()) {

                TRTrackerAnnouncerHelper a = a_it.next();

                if (!announcers.contains(a)) {

                    announcers.add(a);
                }
            }

            if (!is_manual && announcers.size() > 0) {

                if (activated.size() == 0) {

                    TRTrackerAnnouncerHelper a = announcers.get(0);

                    if (Logger.isEnabled()) {
                        Logger.log(new LogEvent(getTorrent(), LOGID, "Activating " + getString(a.getAnnounceSets())));
                    }

                    activated.add(a);

                    last_activation_time = SystemTime.getMonotonousTime();

                    if (provider != null) {

                        to_activate = a;
                    }
                }

                setupActivationCheck(ACT_CHECK_INIT_DELAY);
            }
        }

        if (to_activate != null) {

            if (complete) {

                to_activate.complete(true);

            } else {

                to_activate.update(false);
            }
        }
    }

    protected void setupActivationCheck(int delay) {
        if (announcers.size() > activated.size()) {

            event = SimpleTimer.addEvent("TRMuxer:check", SystemTime.getOffsetTime(delay), new TimerEventPerformer() {
                public void perform(TimerEvent event) {
                    checkActivation(false);
                }
            });
        }
    }

    protected void checkActivation(boolean force) {
        synchronized (this) {

            int next_check_delay;

            if (destroyed || stopped || announcers.size() <= activated.size()) {

                return;
            }

            if (provider == null) {

                next_check_delay = ACT_CHECK_INIT_DELAY;

            } else {

                boolean activate = force;

                boolean seeding = provider.getRemaining() == 0;

                if (seeding && activated.size() > 0) {

                    // when seeding we only activate on tracker fail or major lack of connections
                    // as normally we rely on downloaders rotating and finding us

                    int connected = provider.getConnectedConnectionCount();

                    if (connected < 1) {

                        activate = SystemTime.getMonotonousTime() - last_activation_time >= 60 * 1000;

                        next_check_delay = ACT_CHECK_SEEDING_SHORT_DELAY;

                    } else if (connected < 3) {

                        next_check_delay = ACT_CHECK_SEEDING_LONG_DELAY;

                    } else {

                        next_check_delay = 0;
                    }
                } else {

                    int allowed = provider.getMaxNewConnectionsAllowed();
                    int pending = provider.getPendingConnectionCount();
                    int connected = provider.getConnectedConnectionCount();

                    int online = 0;

                    for (TRTrackerAnnouncerHelper a : activated) {

                        TRTrackerAnnouncerResponse response = a.getLastResponse();

                        if (response != null && response.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE) {

                            online++;
                        }
                    }

                    /*
                     * System.out.println( "checkActivation: announcers=" + announcers.size() + ", active=" + activated.size() + ", online=" + online
                     * + ", allowed=" + allowed + ", pending=" + pending + ", connected=" + connected + ", seeding=" + seeding );
                     */

                    if (online == 0) {

                        activate = true;

                        // no trackers online, start next and recheck soon

                        next_check_delay = ACT_CHECK_INIT_DELAY;

                    } else {

                        int potential = connected + pending;

                        if (potential < 10) {

                            // minimal connectivity

                            activate = true;

                            next_check_delay = ACT_CHECK_INIT_DELAY;

                        } else if (allowed >= 5 && pending < 3 * allowed / 4) {

                            // not enough to fulfill our needs

                            activate = true;

                            next_check_delay = ACT_CHECK_INTERIM_DELAY;

                        } else {
                            // things look good, recheck in a bit

                            next_check_delay = ACT_CHECK_IDLE_DELAY;
                        }
                    }
                }

                if (activate) {

                    for (TRTrackerAnnouncerHelper a : announcers) {

                        if (!activated.contains(a)) {

                            if (Logger.isEnabled()) {
                                Logger.log(new LogEvent(getTorrent(), LOGID, "Activating " + getString(a.getAnnounceSets())));
                            }

                            activated.add(a);

                            last_activation_time = SystemTime.getMonotonousTime();

                            if (complete) {

                                a.complete(true);

                            } else {

                                a.update(false);
                            }

                            break;
                        }
                    }
                }
            }

            if (next_check_delay > 0) {

                setupActivationCheck(next_check_delay);
            }
        }
    }

    private String getString(TOTorrentAnnounceURLSet[] sets) {
        StringBuffer str = new StringBuffer();

        str.append("[");

        int num1 = 0;

        for (TOTorrentAnnounceURLSet s : sets) {

            if (num1++ > 0) {
                str.append(", ");
            }

            str.append("[");

            URL[] urls = s.getAnnounceURLs();

            int num2 = 0;

            for (URL u : urls) {

                if (num2++ > 0) {
                    str.append(", ");
                }

                str.append(u.toExternalForm());
            }

            str.append("]");
        }

        str.append("]");

        return (str.toString());
    }

    private boolean same(TOTorrentAnnounceURLSet[] s1, TOTorrentAnnounceURLSet[] s2) {
        boolean res = sameSupport(s1, s2);

        // System.out.println( "same->" + res + ": " + getString(s1) + "/" + getString(s2));

        return (res);
    }

    private boolean sameSupport(TOTorrentAnnounceURLSet[] s1, TOTorrentAnnounceURLSet[] s2) {
        if (s1.length != s2.length) {

            return (false);
        }

        for (int i = 0; i < s1.length; i++) {

            URL[] u1 = s1[i].getAnnounceURLs();
            URL[] u2 = s2[i].getAnnounceURLs();

            if (u1.length != u2.length) {

                return (false);
            }

            if (u1.length == 1) {

                return (u1[0].toExternalForm().equals(u2[0].toExternalForm()));
            }

            Set<String> set1 = new HashSet<String>();

            for (URL u : u1) {

                set1.add(u.toExternalForm());
            }

            Set<String> set2 = new HashSet<String>();

            for (URL u : u2) {

                set2.add(u.toExternalForm());
            }

            if (!set1.equals(set2)) {

                return (false);
            }
        }

        return (true);
    }

    protected void forceStop(final TRTrackerBTAnnouncerImpl announcer, final String[] networks, final URL url) {
        if (Logger.isEnabled()) {
            Logger.log(new LogEvent(getTorrent(), LOGID, "Force stopping " + url + " as private torrent"));
        }

        new AEThread2("TRMux:fs", true) {
            public void run() {
                try {
                    TRTrackerBTAnnouncerImpl an =
                            new TRTrackerBTAnnouncerImpl(getTorrent(), new TOTorrentAnnounceURLSet[0], networks, true, getHelper());

                    an.cloneFrom(announcer);

                    an.setTrackerURL(url);

                    an.stop(false);

                    an.destroy();

                } catch (Throwable e) {

                }
            }
        }.start();
    }

    private TRTrackerAnnouncerHelper create(TOTorrent torrent, String[] networks, TOTorrentAnnounceURLSet[] sets)

    throws TRTrackerAnnouncerException {
        TRTrackerAnnouncerHelper announcer;

        boolean decentralised;

        if (sets.length == 0) {

            decentralised = TorrentUtils.isDecentralised(torrent.getAnnounceURL());

        } else {

            decentralised = TorrentUtils.isDecentralised(sets[0].getAnnounceURLs()[0]);
        }

        if (decentralised) {

            announcer = new TRTrackerDHTAnnouncerImpl(torrent, networks, is_manual, getHelper());

        } else {

            announcer = new TRTrackerBTAnnouncerImpl(torrent, sets, networks, is_manual, getHelper());
        }

        for (TOTorrentAnnounceURLSet set : sets) {

            URL[] urls = set.getAnnounceURLs();

            for (URL u : urls) {

                String key = u.toExternalForm();

                StatusSummary summary = recent_responses.get(key);

                if (summary == null) {

                    summary = new StatusSummary(announcer, u);

                    recent_responses.put(key, summary);

                } else {

                    summary.setHelper(announcer);
                }
            }
        }

        if (provider != null) {

            announcer.setAnnounceDataProvider(provider);
        }

        if (ip_override != null) {

            announcer.setIPOverride(ip_override);
        }

        return (announcer);
    }

    public TRTrackerAnnouncerResponse getLastResponse() {
        TRTrackerAnnouncerResponse result = null;

        TRTrackerAnnouncerHelper best = getBestActive();

        if (best != null) {

            result = best.getLastResponse();
        }

        if (result == null) {

            result = last_response_informed;
        }

        return (result);
    }

    @Override
    protected void informResponse(TRTrackerAnnouncerHelper helper, TRTrackerAnnouncerResponse response) {
        URL url = response.getURL();

        // can be null for external plugins (e.g. mldht...)

        if (url != null) {

            synchronized (this) {

                String key = url.toExternalForm();

                StatusSummary summary = recent_responses.get(key);

                if (summary != null) {

                    summary.updateFrom(response);
                }
            }
        }

        last_response_informed = response;

        // force recalc of best active next time

        last_best_active_set_time = 0;

        super.informResponse(helper, response);

        if (response.getStatus() != TRTrackerAnnouncerResponse.ST_ONLINE) {

            URL u = response.getURL();

            if (u != null) {

                String s = u.toExternalForm();

                synchronized (failed_urls) {

                    if (failed_urls.contains(s)) {

                        return;
                    }

                    failed_urls.add(s);
                }
            }

            checkActivation(true);
        }
    }

    public boolean isManual() {
        return (is_manual);
    }

    public void setAnnounceDataProvider(TRTrackerAnnouncerDataProvider _provider) {
        List<TRTrackerAnnouncerHelper> to_set;

        synchronized (this) {

            provider = _provider;

            to_set = announcers.getList();
        }

        for (TRTrackerAnnouncer announcer : to_set) {

            announcer.setAnnounceDataProvider(provider);
        }
    }

    protected TRTrackerAnnouncerHelper getBestActive() {
        long now = SystemTime.getMonotonousTime();

        if (now - last_best_active_set_time < 1000) {

            return (last_best_active);
        }

        last_best_active = getBestActiveSupport();

        last_best_active_set_time = now;

        return (last_best_active);
    }

    protected TRTrackerAnnouncerHelper getBestActiveSupport() {
        List<TRTrackerAnnouncerHelper> x = announcers.getList();

        TRTrackerAnnouncerHelper error_resp = null;

        for (TRTrackerAnnouncerHelper announcer : x) {

            TRTrackerAnnouncerResponse response = announcer.getLastResponse();

            if (response != null) {

                int resp_status = response.getStatus();

                if (resp_status == TRTrackerAnnouncerResponse.ST_ONLINE) {

                    return (announcer);

                } else if (error_resp == null && resp_status == TRTrackerAnnouncerResponse.ST_REPORTED_ERROR) {

                    error_resp = announcer;
                }
            }
        }

        if (error_resp != null) {

            return (error_resp);
        }

        if (x.size() > 0) {

            return (x.get(0));
        }

        return (null);
    }

    public URL getTrackerURL() {
        TRTrackerAnnouncerHelper active = getBestActive();

        if (active != null) {

            return (active.getTrackerURL());
        }

        return (null);
    }

    public void setTrackerURL(URL url) {
        List<List<String>> groups = new ArrayList<List<String>>();

        List<String> group = new ArrayList<String>();

        group.add(url.toExternalForm());

        groups.add(group);

        TorrentUtils.listToAnnounceGroups(groups, getTorrent());

        resetTrackerUrl(false);
    }

    public void resetTrackerUrl(boolean shuffle) {
        try {
            split();

        } catch (Throwable e) {

            Debug.out(e);
        }

        for (TRTrackerAnnouncer announcer : announcers) {

            announcer.resetTrackerUrl(shuffle);
        }
    }

    public void setIPOverride(String override) {
        List<TRTrackerAnnouncerHelper> to_set;

        synchronized (this) {

            to_set = announcers.getList();

            ip_override = override;
        }

        for (TRTrackerAnnouncer announcer : to_set) {

            announcer.setIPOverride(override);
        }
    }

    public void clearIPOverride() {
        List<TRTrackerAnnouncerHelper> to_clear;

        synchronized (this) {

            to_clear = announcers.getList();

            ip_override = null;
        }

        for (TRTrackerAnnouncer announcer : to_clear) {

            announcer.clearIPOverride();
        }
    }

    public void setRefreshDelayOverrides(int percentage) {
        for (TRTrackerAnnouncer announcer : announcers) {

            announcer.setRefreshDelayOverrides(percentage);
        }
    }

    public int getTimeUntilNextUpdate() {
        TRTrackerAnnouncerHelper active = getBestActive();

        if (active != null) {

            return (active.getTimeUntilNextUpdate());
        }

        return (Integer.MAX_VALUE);
    }

    public int getLastUpdateTime() {
        TRTrackerAnnouncerHelper active = getBestActive();

        if (active != null) {

            return (active.getLastUpdateTime());
        }

        return (0);
    }

    public void update(boolean force) {
        List<TRTrackerAnnouncerHelper> to_update;

        synchronized (this) {

            to_update = is_manual ? announcers.getList() : new ArrayList<TRTrackerAnnouncerHelper>(activated);
        }

        for (TRTrackerAnnouncer announcer : to_update) {

            announcer.update(force);
        }
    }

    public void complete(boolean already_reported) {
        List<TRTrackerAnnouncerHelper> to_complete;

        synchronized (this) {

            complete = true;

            to_complete = is_manual ? announcers.getList() : new ArrayList<TRTrackerAnnouncerHelper>(activated);
        }

        for (TRTrackerAnnouncer announcer : to_complete) {

            announcer.complete(already_reported);
        }
    }

    public void stop(boolean for_queue) {
        List<TRTrackerAnnouncerHelper> to_stop;

        synchronized (this) {

            stopped = true;

            to_stop = is_manual ? announcers.getList() : new ArrayList<TRTrackerAnnouncerHelper>(activated);

            activated.clear();
        }

        for (TRTrackerAnnouncer announcer : to_stop) {

            announcer.stop(for_queue);
        }
    }

    public void destroy() {
        TRTrackerAnnouncerFactoryImpl.destroy(this);

        List<TRTrackerAnnouncerHelper> to_destroy;

        synchronized (this) {

            destroyed = true;

            to_destroy = announcers.getList();
        }

        for (TRTrackerAnnouncer announcer : to_destroy) {

            announcer.destroy();
        }

        TimerEvent ev = event;

        if (ev != null) {

            ev.cancel();
        }
    }

    public int getStatus() {
        TRTrackerAnnouncer max_announcer = getBestAnnouncer();

        return (max_announcer == null ? -1 : max_announcer.getStatus());
    }

    public String getStatusString() {
        TRTrackerAnnouncer max_announcer = getBestAnnouncer();

        return (max_announcer == null ? "" : max_announcer.getStatusString());
    }

    public TRTrackerAnnouncer getBestAnnouncer() {
        int max = -1;

        TRTrackerAnnouncer max_announcer = null;

        for (TRTrackerAnnouncer announcer : announcers) {

            int status = announcer.getStatus();

            if (status > max) {

                max_announcer = announcer;
                max = status;
            }
        }

        return (max_announcer == null ? this : max_announcer);
    }

    public void refreshListeners() {
        informURLRefresh();
    }

    public void setAnnounceResult(DownloadAnnounceResult result) {
        // this is only used for setting DHT results

        for (TRTrackerAnnouncer announcer : announcers) {

            if (announcer instanceof TRTrackerDHTAnnouncerImpl) {

                announcer.setAnnounceResult(result);

                return;
            }
        }

        // TODO: we should always create a DHT entry and have it denote DHT tracking for all circustances
        // have the DHT plugin set it to offline if disabled

        List<TRTrackerAnnouncerHelper> x = announcers.getList();

        if (x.size() > 0) {

            x.get(0).setAnnounceResult(result);
        }
    }

    protected int getPeerCacheLimit() {
        synchronized (this) {

            if (activated.size() < announcers.size()) {

                return (0);
            }
        }

        if (SystemTime.getMonotonousTime() - create_time < 15 * 1000) {

            return (0);
        }

        TRTrackerAnnouncer active = getBestActive();

        if (active != null && provider != null && active.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE) {

            if (provider.getMaxNewConnectionsAllowed() > 0 && provider.getPendingConnectionCount() == 0) {

                return (5);

            } else {

                return (0);
            }
        }

        return (10);
    }

    public TrackerPeerSource getTrackerPeerSource(final TOTorrentAnnounceURLSet set) {
        URL[] urls = set.getAnnounceURLs();

        final String[] url_strs = new String[urls.length];

        for (int i = 0; i < urls.length; i++) {

            url_strs[i] = urls[i].toExternalForm();
        }

        return (new TrackerPeerSource() {
            private StatusSummary _summary;
            private boolean enabled;
            private long fixup_time;

            private StatusSummary fixup() {
                long now = SystemTime.getMonotonousTime();

                if (now - fixup_time > 1000) {

                    long most_recent = 0;
                    StatusSummary summary = null;

                    synchronized (TRTrackerAnnouncerMuxer.this) {

                        for (String str : url_strs) {

                            StatusSummary s = recent_responses.get(str);

                            if (s != null) {

                                if (summary == null || s.getTime() > most_recent) {

                                    summary = s;
                                    most_recent = s.getTime();
                                }
                            }
                        }
                    }

                    if (provider != null) {

                        enabled = provider.isPeerSourceEnabled(PEPeerSource.PS_BT_TRACKER);
                    }

                    if (summary != null) {

                        _summary = summary;
                    }

                    fixup_time = now;
                }

                return (_summary);
            }

            public int getType() {
                return (TrackerPeerSource.TP_TRACKER);
            }

            public String getName() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    String str = summary.getURL().toExternalForm();

                    int pos = str.indexOf('?');

                    if (pos != -1) {

                        str = str.substring(0, pos);
                    }

                    return (str);
                }

                return (url_strs[0]);
            }

            public int getStatus() {
                StatusSummary summary = fixup();

                if (!enabled) {

                    return (ST_DISABLED);
                }

                if (summary != null) {

                    return (summary.getStatus());
                }

                return (ST_QUEUED);
            }

            public String getStatusString() {
                StatusSummary summary = fixup();

                if (summary != null && enabled) {

                    return (summary.getStatusString());
                }

                return (null);
            }

            public int getSeedCount() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getSeedCount());
                }

                return (-1);
            }

            public int getLeecherCount() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getLeecherCount());
                }

                return (-1);
            }

            public int getCompletedCount() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getCompletedCount());
                }

                return (-1);
            }

            public int getPeers() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getPeers());
                }

                return (-1);
            }

            public int getLastUpdate() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    long time = summary.getTime();

                    if (time == 0) {

                        return (0);
                    }

                    long elapsed = SystemTime.getMonotonousTime() - time;

                    return ((int) ((SystemTime.getCurrentTime() - elapsed) / 1000));
                }

                return (0);
            }

            public int getSecondsToUpdate() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getSecondsToUpdate());
                }

                return (-1);
            }

            public int getInterval() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    return (summary.getInterval());
                }

                return (-1);
            }

            public int getMinInterval() {
                StatusSummary summary = fixup();

                if (summary != null && enabled) {

                    return (summary.getMinInterval());
                }

                return (-1);
            }

            public boolean isUpdating() {
                StatusSummary summary = fixup();

                if (summary != null && enabled) {

                    return (summary.isUpdating());
                }

                return (false);
            }

            public boolean canManuallyUpdate() {
                StatusSummary summary = fixup();

                if (summary == null) {

                    return (false);
                }

                return (summary.canManuallyUpdate());
            }

            public void manualUpdate() {
                StatusSummary summary = fixup();

                if (summary != null) {

                    summary.manualUpdate();
                }
            }

            public boolean canDelete() {
                return (false);
            }

            public void delete() {
                Debug.out("derp");
            }
        });
    }

    public void generateEvidence(IndentWriter writer) {
        for (TRTrackerAnnouncer announcer : announcers) {

            announcer.generateEvidence(writer);
        }
    }

    private static class StatusSummary {
        private TRTrackerAnnouncerHelper helper;

        private long time;
        private URL url;
        private int status;
        private String status_str;
        private int seeds = -1;
        private int leechers = -1;
        private int peers = -1;
        private int completed = -1;

        private int interval;
        private int min_interval;

        protected StatusSummary(TRTrackerAnnouncerHelper _helper, URL _url) {
            helper = _helper;
            url = _url;

            status = TrackerPeerSource.ST_QUEUED;
        }

        protected void setHelper(TRTrackerAnnouncerHelper _helper) {
            helper = _helper;
        }

        protected void updateFrom(TRTrackerAnnouncerResponse response) {
            time = SystemTime.getMonotonousTime();

            int state = response.getStatus();

            if (state == TRTrackerAnnouncerResponse.ST_ONLINE) {

                status = TrackerPeerSource.ST_ONLINE;

                seeds = response.getScrapeCompleteCount();
                leechers = response.getScrapeIncompleteCount();
                completed = response.getScrapeDownloadedCount();
                peers = response.getPeers().length;

            } else {

                status = TrackerPeerSource.ST_ERROR;
            }

            status_str = response.getStatusString();

            interval = (int) helper.getInterval();
            min_interval = (int) helper.getMinInterval();
        }

        public long getTime() {
            return (time);
        }

        public URL getURL() {
            return (url);
        }

        public int getStatus() {
            return (status);
        }

        public String getStatusString() {
            return (status_str);
        }

        public int getSeedCount() {
            return (seeds);
        }

        public int getLeecherCount() {
            return (leechers);
        }

        public int getCompletedCount() {
            return (completed);
        }

        public int getPeers() {
            return (peers);
        }

        public boolean isUpdating() {
            return (helper.isUpdating());
        }

        public int getInterval() {
            return (interval);
        }

        public int getMinInterval() {
            return (min_interval);
        }

        public int getSecondsToUpdate() {
            return (helper.getTimeUntilNextUpdate());
        }

        public boolean canManuallyUpdate() {
            return (((SystemTime.getCurrentTime() / 1000 - helper.getLastUpdateTime() >= TRTrackerAnnouncer.REFRESH_MINIMUM_SECS)));
        }

        public void manualUpdate() {
            helper.update(true);
        }
    }
}
